package com.appspot.seqpop.model;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;

import org.slim3.datastore.Attribute;
import org.slim3.datastore.InverseModelListRef;
import org.slim3.datastore.Model;
import org.slim3.datastore.ModelRef;

@Model(schemaVersion = 1)
public class Alignment implements Serializable, Iterable<AlignedSequence> {

    private static final long serialVersionUID = 1L;

    @Attribute(primaryKey = true)
    private Key key;

    @Attribute(version = true)
    private Long version;
    
    private Date createdDate = new Date(); 
    private Date lastModifiedDate = createdDate;

    @Attribute(persistent = false)
    private InverseModelListRef<AlignedSequence, Alignment> alignmentRef =
        new InverseModelListRef<AlignedSequence, Alignment>(AlignedSequence.class, "alignmentRef", this);
    
    private ModelRef<Result> resultRef = new ModelRef<Result>(Result.class);
    
    @Attribute(lob = true)
    private AlignmentDigest digest;
    
    /**
     * Returns the key.
     *
     * @return the key
     */
    public Key getKey() {
        return key;
    }

    /**
     * Sets the key.
     *
     * @param key
     *            the key
     */
    public void setKey(Key key) {
        this.key = key;
    }

    /**
     * Returns the version.
     *
     * @return the version
     */
    public Long getVersion() {
        return version;
    }

    /**
     * Sets the version.
     *
     * @param version
     *            the version
     */
    public void setVersion(Long version) {
        this.version = version;
    }
    
    public Date getCreatedDate() {
        return this.createdDate;
    }

    public void setCreatedDate(Date createdDate) {
        this.createdDate = createdDate;
    }

    public Date getLastModifiedDate() {
        return this.lastModifiedDate;
    }

    public void setLastModifiedDate(Date lastModifiedDate) {
        this.lastModifiedDate = lastModifiedDate;
    }

    public ModelRef<Result> getResultRef() {
        return resultRef;
    }

    public InverseModelListRef<AlignedSequence, Alignment> getAlignmentRef() {
        return this.alignmentRef;
    }

    public List<AlignedSequence> getAlignment() {
        return getAlignmentRef().getModelList();
    }

    public int getLength() {
        int length = -1;
        for (AlignedSequence sequence : getAlignment()) {
            int position = sequence.getPosition();
            int tail = position + sequence.getLength();
            if (tail > length) {
                length = tail;
            }
        }
        return length;
    }

    public int getSequenceCount() {
        return getAlignment().size();
    }

    public Object[] getElementsAt(int index) {
        int length = getLength();
        if (index < 0 || length <= index) {
            throw new IndexOutOfBoundsException();
        }

        Object[] elements = new Object[getSequenceCount()];
        int sequenceIndex = 0;
        for (AlignedSequence sequence : this) {
            int nucleotideIndex = index - sequence.getPosition();
            int sequenceLength = sequence.getLength();
            if (0 <= nucleotideIndex && nucleotideIndex < sequenceLength) {
                elements[sequenceIndex] = sequence.getElementAt(nucleotideIndex);
            } else {
                elements[sequenceIndex] = Sequence.GAP;
            }
            sequenceIndex++;
        }
        return elements;
    }

    public int getPosition(Sequence sequence) {
        for (AlignedSequence s : this) {
            if (s.equals(sequence)) {
                return s.getPosition();
            }
        }
        return AlignedSequence.INVALID_POSITION;
    }

    public int setPosition(Sequence sequence, int position) {
        for (AlignedSequence s : this) {
            if (s.equals(sequence)) {
                s.setPosition(position);
                if (position < 0) {
                    shift(-position);
                    return 0;
                }
                return position;
            }
        }
        return AlignedSequence.INVALID_POSITION;
    }

    public AlignmentDigest getDigest() {
        return this.digest;
    }

    public void setDigest(AlignmentDigest digest) {
        this.digest = digest;
    }
    
    public Map<String, Object> asJson() {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("key", KeyFactory.keyToString(getKey()));
        map.put("result", getResultRef().getModel().asJson());
        List<AlignedSequence> alignment = getAlignment();
        String[] sequences = new String[alignment.size()];
        for (int index = 0; index < sequences.length; index++) {
            sequences[index] = alignment.get(index).getSequence();
        }
        map.put("sequences", sequences);
        return map;
    }

    public AlignedSequence add(Sequence sequence) {
        return add(sequence, 0);
    }

    public AlignedSequence add(Sequence sequence, int position) {
        if (sequence == null) {
            throw new IllegalArgumentException();
        }

        if (isEmpty()) {
            AlignedSequence aligned = new AlignedSequence(sequence, 0);
            add(aligned);
            return aligned;
        } else {
            AlignedSequence aligned = new AlignedSequence(sequence, position);
            add(aligned);
            if (position < 0) {
                shift(-position);
            }
            return aligned;
        }
    }
    
    private void add(AlignedSequence aligned) {
        aligned.getAlignmentRef().setModel(this);
    }

    private void shift(int value) {
        for (AlignedSequence sequence : this) {
            int oldPosition = sequence.getPosition();
            int newPosition = oldPosition + value;
            sequence.setPosition(newPosition);
        }
    }

    public boolean isEmpty() {
        return getAlignment().isEmpty();
    }

    public void clear() {
        getAlignment().clear();
    }

    public Iterator<AlignedSequence> iterator() {
        return getAlignment().iterator();
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((key == null) ? 0 : key.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        Alignment other = (Alignment) obj;
        if (key == null) {
            if (other.key != null) {
                return false;
            }
        } else if (!key.equals(other.key)) {
            return false;
        }
        return true;
    }
}
